/*
 * Copyright 2010
 * Jairou Software Organization
 * All Rights Reserved.
 * 
 * This software is the proprietary 
 * information of Roger Ramia
 * and the Jairou Software Organization 
 * Use is subject to license terms.
 * 
 * For licensing information please visit
 * http://www.jairou.org/license
 * 
 * @author Roger Ramia
 */
package org.jairou.core;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.sql.Date;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;



/**
 * Core object reflection utilities.
 * Provides normalized access (get, set, find, containsKey and keys) 
 * to most object types. Also provides basic introspection utilities
 * <br/>
 * Object types supported include:
 * All Standard Beans, Any objects implementing Map,
 * HttpServletRequest, HttpSession and ServletContext			   
 *
 * @author Roger Ramia 
 */
public class Objects {
	
	// cached keys by Class for repeated reflection calls
	private static Map<Class, Collection<String>> pojoKeys = new Hashtable();
	
	
	/**
	 * given a dot notation expression, 
	 * loops through the dots and resolves the value 
	 * from the provided context
	 * 
	 * @param key - the dot notation path of the property to find
	 * 				if the key contains no dots, a simple get is
	 * 				called on the provided context
	 * @param context - the object which to perform the find
	 * @return - the value of the specified property or null if the property
	 * 			 does not exist
	 */
	public static Object find(String key, Object context) {
		Object out = null;
		if (!Strings.nullOrEmpty(key) && context != null) {
			if (key.indexOf(".") != -1) {
				StringTokenizer st = new StringTokenizer(key, ".");
				Object currContext = context;
				while (st.hasMoreTokens()) {
					String property = st.nextToken();
					out = Objects.get(property, currContext);
					currContext = out;
				}
			}
			else out = Objects.get(key, context);
		}
		return out;
	}
	
	/**
	 * Determines the type of the provided object then calls the appropriate get
	 * method with the specified key.  If the type can not be determined, 
	 * attempts to call the assumed getter method via reflection.  
	 * If no getter is found for the specified key, the exception is swallowed
	 * and null is returned.
	 * 
	 * @param key - the name of the property you want to get
	 * @param obj - the object to get it from
	 * @return - the value of the property or null if the property does not exist
	 */
	public static Object get(String key, Object obj) {
		Object out = null;
		if (key != null && obj != null) {
			try {
				if (obj instanceof Map) out = ((Map)obj).get(key);
				
				// get from ServletRequest
				else if (obj instanceof HttpServletRequest) {
					out = ((HttpServletRequest) obj).getParameter(key);
					if (out == null) 
						out = ((HttpServletRequest) obj).getAttribute(key);
				}
				// get from an HttpSession
				else if (obj instanceof HttpSession) out = ((HttpSession) obj).getAttribute(key);
				
				// get from an ServletContext
				else if (obj instanceof ServletContext) out = ((ServletContext) obj).getAttribute(key);		
				
				// assume object is a pojo and has a getXXX method
				else if (!isNative(obj)){				
				
					Method getter = null;
					Class cls = obj.getClass();
					String methodName ="get"+Strings.initUpper(key);
					try {
						getter = cls.getMethod(methodName, new Class[]{});
					}
					catch(Exception e) {
						methodName = "is"+Strings.initUpper(key);
						getter = cls.getMethod(methodName);
					}
					if (getter != null && isGetter(getter)) {
						out = getter.invoke(obj);
					}
				}
			}
			catch(Exception e) {
				e.printStackTrace();
			}
		}
		return out;
	}
	
	
	/**
	 * Determines the type of the provided object then calls the appropriate set
	 * method with the specified key and value.  If the type can not be determined, 
	 * attempts to call the assumed setter method via reflection.  
	 * If no setter is found for the specified key, the exception is swallowed
	 * and nothing is set.
	 *
	 * @param key - name of the property to be set
	 * @param val - value of the property to be set
	 * @param obj - object to set the value on
	 */
	public static void set(String key, Object val, Object obj) {
		if (key != null && obj != null) {
			try {
				if (obj instanceof Map) ((Map)obj).put(key, val);
				
				// sets a HttpServletRequest Attribute
				else if (obj instanceof HttpServletRequest) 
					((HttpServletRequest) obj).setAttribute(key, val);
				
				// sets a HttpSession Attribute
				else if (obj instanceof HttpSession) 
					((HttpSession) obj).setAttribute(key, val);
				
				// sets a ServletContext Attribute
				else if (obj instanceof ServletContext) 
					((ServletContext) obj).setAttribute(key, val);
				
				else if (!isNative(obj)) {
					Method setter = null;
					Class cls = obj.getClass();
					String methodName ="set"+Strings.initUpper(key);
					setter = cls.getMethod(methodName, new Class[]{val.getClass()});	
					if (setter != null && isSetter(setter)) {
						setter.invoke(obj, new Object[]{val});
					}
				}
			}
			catch(Exception e) {
				System.out.println("no setter found for property:"+key+
									" on class: "+((obj != null) ? obj.getClass() : "null" )+
									" accepting parameter: "+((val != null) ? val.getClass() : "null"));
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * Determines the type of the provided object then calls the appropriate method
	 * to return the objects keys.  If the type can not be determined, 
	 * attempts to loop the assumed getter method via reflection to build a list of
	 * public properties. If no getters are found simply returns null
	 * <br/>
	 * Any keys obtained through reflection are cached by type so that
	 * introspection is only performed once per type.
	 * 
	 * @param obj - the object to obtain the keys from.
	 * @return - a collection of strings representing the public properties
	 * 			 of the provided object
	 */
	public static Collection<String> keys(Object obj) {
		Collection<String> keys = null;
		if (obj != null) {
			// gets keys for map
			if (obj instanceof Map) keys = ((Map)obj).keySet();
			
			// gets keys for HttpServletRequest
			else if (obj instanceof HttpServletRequest) {
				HttpServletRequest req = ((HttpServletRequest) obj);
				keys = toKeys(req.getAttributeNames());
				keys.addAll(toKeys(req.getParameterNames()));
			}
			
			// gets keys for HttpSession
			else if (obj instanceof HttpSession) 
				keys = toKeys(((HttpSession) obj).getAttributeNames());
			
			// gets keys for ServletContext
			else if (obj instanceof ServletContext) 
				keys = toKeys(((ServletContext) obj).getAttributeNames());
			
			// reflects on POJO getters for keys 
			else if (!isNative(obj)){
				Class cls = obj.getClass();
				// look for cached keys
				if (pojoKeys.containsKey(cls)) keys = pojoKeys.get(cls);
				else { // reflect on all the public getters
					Method[] methods = cls.getMethods();
					if (methods != null) keys = new ArrayList<String>();
					for(Method method : methods) {
						if (isGetter(method)) {						
							String name = method.getName();
							String key = name.startsWith("get") ?
									     Strings.initLower(name.substring(3)) :
									     Strings.initLower(name.substring(2));
							keys.add(key);
						}
					}
					pojoKeys.put(cls, keys);
				}
			}
		}
		return keys;
	}
	
	/**
	 * Checks to see if the object contains the specified key
	 * <br/>
	 * Note: If the object is a POJO, and the keys have 
	 * never been obtained by a call to keys, this method 
	 * will incur the initial cost of reflecting all the public 
	 * getters to create the cached list of keys.  Repeated calls 
	 * to this method will not reflect each time.
	 * 
	 * @param key - the key to check
	 * @param obj - the object to search for the specified key
	 * @return boolean indicating if the key is contained in the provided object
	 */
	public static boolean containsKey(String key, Object obj) {
		if (!Strings.nullOrEmpty(key) && obj != null) {
			Collection<String> keys = keys(obj);
			if (keys != null) return keys.contains(key);
		}
		return false;
	}
	
	/**
	 * reflection utility indicating if the supplied method is a getter
	 * a getter is defined by a method which starts with the word
	 * 'get' or 'is', has no parameters and is not the 
	 * root object's getClass method
	 * 
	 * @param method - method object to check
	 * @return - boolean indicating if supplied method is a getter
	 */
	public static boolean isGetter(Method method) {
		boolean out = false;
		if (method != null) {
			String name = method.getName();
			out = (name.startsWith("get") || name.startsWith("is")) &&
				   method.getParameterTypes().length == 0 &&
				   !method.getName().equalsIgnoreCase("getClass");
		}
		return out;
	}
	
	/**
	 * reflection utility indicating if the supplied method is a setter
	 * a setter is defined by a method which starts with the word
	 * 'set', has a single parameter and returns void
	 * 
	 * @param method - method object to check
	 * @return - boolean indicating if supplied method is a setter
	 */
	public static boolean isSetter(Method method) {
		boolean out = false;
		if (method != null) {
			String name = method.getName();
			out = (name.startsWith("set") &&
				   method.getParameterTypes().length == 1 &&
				   void.class.equals(method.getReturnType()));
		}
		return out;
	}
	
	/**
	 * indicates if the given object
	 * is a native java object.
	 * A native object is defined as primitive
	 * value wrapper or a date
	 * 
	 * @param obj - object to check
	 * @return boolean indicating if the object is native
	 */
	public static boolean isNative(Object obj) {
		return (obj instanceof String ||
				obj instanceof Character ||
				obj instanceof Byte ||
				obj instanceof Short ||
				obj instanceof Boolean ||
				obj instanceof Number || 
				obj instanceof java.util.Date || 
				obj instanceof java.sql.Date ||
				obj instanceof Timestamp);
	}
	
	/**
	 * indicates if the given object 
	 * derives from the Number Class
	 * 
	 * @param obj - object to check
	 * @return boolean indicating if the object is numeric
	 */
	public static boolean isNumeric(Object obj) {
		return (obj instanceof Number);
	}
	
	/**
	 * indicates if the object is a boolean type
	 * @param obj
	 * @return
	 */
	public static boolean isBoolean(Object obj) {
		return (obj instanceof Boolean);
	}
	
	/**
	 * Attempts to convert an Object of one type to another
	 * by trying to invoke the constructor of the class
	 * passing the provided object. 
	 * <br/>
	 * For example converts an String to an Integer
	 * by calling Integer(String)
	 * <br/>
	 * If the appropriate constructor does not 
	 * exist or the object is already an instance of 
	 * the requested class, the original object is simply returned. 
	 * 
	 * @param clsName - class name of requested type
	 * @param val - original value
	 * @return - resulting object
	 * @throws Exception
	 */
	public static Object load(String clsName, Object val) 
	throws Exception {
		Object out = null;
		if (clsName != null) {
			Class cls = toWrapperClass(clsName);
			if (cls != null) {
				// if the value is already the target return the original value
				if (cls.isInstance(val)) out = val;		
				
				else if (val != null) {
					// check if target is a date
					if (isDateClass(cls)) {
						out = toDate(val);
					}
					// check if target is native time
					else if (isNativeClass(cls)) { 
						// try passing the original value to the constructor
						Constructor constr = cls.getConstructor(new Class<?>[]{val.getClass()});
						if (constr != null) out = constr.newInstance(new Object[]{val});
					}
					// assume target is a POJO and load all properties 
					else {						
						Object target = cls.newInstance();
						for(String key : keys(target))
							set(key, get(key, val), target);
					}
				}
				else {
					if (clsName.equals("int") || 
						clsName.equals("long") ||
						clsName.equals("float") ||
						clsName.equals("double") ||
						clsName.equals("boolean") ||
						clsName.equals("short") || 
						clsName.equals("byte") ) {
						Constructor constr = cls.getConstructor(new Class<?>[]{String.class});
						if (constr != null) out = constr.newInstance(new Object[]{"0"});
					}
					if (clsName.equals("char")) { 
						Constructor constr = cls.getConstructor(new Class<?>[]{char.class});
						if (constr != null) out = constr.newInstance(new Object[]{'\u0000'});
					}
				}
			}
		}
		return out;		
	}
	
	
	/**
	 * given a string name
	 * returns the appropriate wrapper class
	 * @param type
	 * @return
	 * @throws Exception
	 */	
	public static Class toWrapperClass(String name) 
	throws Exception {
		if ("int".equals(name)) name = "java.lang.Integer";
		else if ("long".equals(name)) name = "java.lang.Long";
		else if ("double".equals(name)) name = "java.lang.Double";
		else if ("float".equals(name)) name = "java.lang.Float";
		else if ("char".equals(name)) name = "java.lang.Character";
		else if ("byte".equals(name)) name = "java.lang.Byte";
		else if ("short".equals(name)) name = "java.lang.Short";
		else if ("boolean".equals(name)) name = "java.lang.Boolean";
		return toClass(name);
	}
	
	/**
	 * given a string name
	 * returns the appropriate class
	 * @param type
	 * @return
	 * @throws Exception
	 */	
	public static Class toClass(String name) 
	throws Exception {
		if ("int".equals(name)) return int.class;
		else if ("long".equals(name)) return long.class;
		else if ("double".equals(name)) return double.class;
		else if ("float".equals(name)) return float.class;
		else if ("char".equals(name)) return char.class;
		else if ("byte".equals(name)) return byte.class;
		else if ("short".equals(name)) return short.class;
		else if ("boolean".equals(name)) return boolean.class;
		else return Class.forName(name);
	}
	
	/**
	 * converts an String value into a java.sql.Date
	 * using the date format defined in the 
	 * db-type.date.format xml
	 * 
	 * @param val - String representation of date
	 * @return - java.sql.Date instance
	 * @throws Exception
	 */
	public static Date toDate(Object val)
	throws Exception {
		String format = XML.get("db-type.date.format");
		return toDate(val, format);
	}
	
	
	/**
	 * checks to see if the class is a Date
	 * or Timestamp
	 * @param cls
	 * @return
	 */
	public static boolean isDateClass(Class cls) {
		if (cls != null) {
			return (cls.equals(java.sql.Date.class) ||
					cls.equals(Timestamp.class) ||
					cls.equals(java.util.Date.class));
		}
		return false;
	}
	
	/**
	 * converts an String value into a java.sql.Date
	 * using the provided date format string
	 * all valid java.text.SimpleDateFormat
	 * formats are accepted
	 * 
	 * @param val - String representation of date
	 * @param format - String representation of format to be parsed
	 * @return - java.sql.Date instance
	 * @throws Exception
	 */
	public static Date toDate(Object val, String format)
	throws Exception {
		if (val != null && format != null) {
			try {
				SimpleDateFormat sf = new SimpleDateFormat(format);
				return new Date(sf.parse(val.toString()).getTime());
			}
			catch (Throwable t) {
				throw new Exception("unable to convert "+val+" to date format "+format+" "+t.getMessage());
			}
		}
		else throw new Exception("Unable to create date from value:"+val+" using format:"+format);
	}
	
	public static Object newInstance(String clsName) 
	throws Exception {
		return Class.forName(clsName).newInstance();
	}
	
	/**
	 * indicates if the given Class
	 * is a native java Class
	 * 
	 * @param obj
	 * @return
	 */
	public static boolean isNativeClass(Class cls) {
		return (cls != null && 
			   (String.class.isAssignableFrom(cls) ||
				Character.class.isAssignableFrom(cls) ||
				Byte.class.isAssignableFrom(cls) ||
				Short.class.isAssignableFrom(cls) ||
				Boolean.class.isAssignableFrom(cls) ||
				Number.class.isAssignableFrom(cls) || 
				Date.class.isAssignableFrom(cls)));
	}
	
	
	/**
	 * takes an enumeration of names and converts into a
	 * collection of keys.  Handy for converting Http scope
	 * object attributes to a collection of string keys
	 * 
	 * @param names
	 * @return
	 */
	public static Collection<String> toKeys(Enumeration names) {
		Collection keys = null;
		if (names != null) {
			keys = new ArrayList<String>();
			while (names.hasMoreElements()) {
				keys.add(names.nextElement().toString());
			}
		}
		return keys;
	}
}

